#!/bin/sh

change_file="CHANGELOG.md"
xdg_data_home=${XDG_DATA_HOME:-$HOME/.local/share}
auth_dir="$xdg_data_home/change/"
auth_file="$auth_dir/auth"
change_version="0.14.4"
nl="
"

Usage() {
    echo "usage: change [COMMAND [ARGS]]"
    echo
    echo "change [--bump PATH]"
    echo "    updates an existing $change_file"
    echo "    optional: --bump passes the newest version as an arg to a script at PATH"
    echo
    echo "change init"
    echo "    creates a $change_file with the first version"
    echo
    echo "change tag [-p]"
    echo "    tags the latest local commit with the lastest $change_file version"
    echo "    optional: -p also pushes that tag to origin"
    echo
    echo "change auth [--token TOKEN]"
    echo "    prompts you for a personal access token for posting releases"
    echo "    optional: --token uses TOKEN instead of asking for it interactively"
    echo
    echo "change post [--dry-run]"
    echo "    posts a GitHub release for the latest version in $change_file"
    echo "    optional: --dry-run prints url, version, and body without sending"
    echo
    echo "change all [--bump PATH]"
    echo "    runs change, then opens $change_file in \$EDITOR"
    echo "    commits and pushes the $change_file to origin"
    echo "    runs change tag -p and then runs change post"
    echo "    optional: --bump passes the newest version as an arg to a script at PATH"
    echo
    echo "change version"
    echo "    prints which version of the change tool is being used"
}

#### Tags ####

get_git_tags() {
    reverse=${1:-v} # send arg "-v" to reverse the list
    git tag --list "[0-9]*\.[0-9]*.*" "v[0-9]*\.[0-9]*.*" --sort="${reverse}:refname"
}

get_tag_date() {
    date=$(git log -1 --pretty=format:"%ci" "$1" 2>/dev/null | cut -d " " -f 1)
    [ "$date" ] && { echo "$date"; return; }
    date +%F
}

get_git_url() {
    git config --get remote.origin.url |
        sed -e "s|^git@\(.*\.com\):\(.*\)|https://\1/\2|" -e "s|\(.*\)\.git|\1|"
}

get_latest_log_tag() {
    sed -n "s|^\[Unreleased\]:.*/\(.*\)\.\.\.HEAD$|\1|p" $change_file
}

rm_tag_prefix() {
    echo "$1" | sed "s|^[^0-9]*\([0-9.]*.*\)|\1|"
}

inc_tag_major() {
    cur_major=$(echo "$1" | sed "s|^[^0-9]*\([0-9]*\)\..*$|\1|")
    echo "$1" | sed "s|\.[0-9]*|.0|g" |
        sed "s|\(^[^0-9]*\)[0-9]*\(\..*$\)|\1$(( cur_major + 1 ))\2|"
}

inc_tag_minor() {
    cur_minor=$(echo "$1" | sed "s|^.*\.\([0-9]*\)\..*$|\1|")
    echo "$1" | sed "s|\.[0-9]*|.0|g" |
        sed "s|\(^.*\.\)[0-9]*\(\..*$\)|\1$(( cur_minor + 1 ))\2|"
}

inc_tag_patch() {
    cur_patch=$(echo "$1" | sed "s|^.*\.\([0-9]*\).*$|\1|")
    echo "$1" | sed "s|\(^.*\.\)[0-9]*\(.*$\)|\1$(( cur_patch + 1 ))\2|"
}

rm_tag_suffix() {
    sed "s|\(^[^0-9]*[0-9.]*\).*|\1|"
}

#### Commits ####

filter_major_commits() {
    args="-e" # send arg "-v" for inverse and "-q" for quiet
    [ "$1" ] && args="${1}e"

    grep "$args" "^[A-Za-z]*!: " \
        -e "^[A-Za-z]*(.*)!: " \
        -e "^[*-] *[A-Za-z]*!: " \
        -e "^[*-] *[A-Za-z]*(.*)!: " \
        -e "^BREAKING[ -]CHANGE: " \
        -e "^[*-] *BREAKING[ -]CHANGE: " \
        -e "^BREAKING[ -]CHANGE"
}

filter_minor_commits() {
    args="-e" # send arg "-v" for inverse and "-q" for quiet
    [ "$1" ] && args="${1}e"

    grep -i "$args" "^feat: " \
        -e "^feat(.*): " \
        -e "^[*-] *feat: " \
        -e "^[*-] *feat(.*): "
}

filter_patch_commits() {
    args="-e" # send arg "-v" for inverse and "-q" for quiet
    [ "$1" ] && args="${1}e"

    grep -i "$args" "^fix: " \
        -e "^fix(.*): " \
        -e "^[*-] *fix: "\
        -e "^[*-] *fix(.*): "
}

filter_conv_commits() {
    grep -e "^[A-Za-z]*: " -e "^[A-Za-z]*(.*): " \
        -e "^[*-] *[A-Za-z]*: " -e "^[*-] *[A-Za-z]*(.*): " \
        -e "^[A-Za-z]*!: " -e "^[A-Za-z]*(.*)!: " \
        -e "^[*-] *[A-Za-z]*!: " -e "^[*-] *[A-Za-z]*(.*)!: " \
        -e "^BREAKING[ -]CHANGE: " -e "^[*-] *BREAKING[ -]CHANGE: " \
        -e "^BREAKING[ -]CHANGE"
}

fmt_conv_type() {
    sed -e "s|^[A-Za-z]*: \(.*$\)|\1|g" \
        -e "s|^[A-Za-z]*(\(.*\)): \(.*$\)|\2 for \1|g" \
        -e "s|^[*-] *[A-Za-z]*: \(.*$\)|\1|g" \
        -e "s|^[*-] *[A-Za-z]*(\(.*\)): \(.*$\)|\2 for \1|g" \
        -e "s|^[A-Za-z]*!: \(.*$\)|\1|g" \
        -e "s|^[A-Za-z]*(\(.*\))!: \(.*$\)|\2 for \1|g" \
        -e "s|^[*-] *[A-Za-z]*!: \(.*$\)|\1|g" \
        -e "s|^[*-] *[A-Za-z]*(\(.*\))!: \(.*$\)|\2 for \1|g" \
        -e "s|^BREAKING[ -]CHANGE: \(.*$\)|\1|g" \
        -e "s|^[*-] *BREAKING[ -]CHANGE: \(.*$\)|\1|g" \
        -e "s|^BREAKING[ -]CHANGE|a breaking change|g"
}

fmt_commits() {
    all_commits=$(echo "$1" | fmt_conv_type)

    IFS="$nl"
    for commit in $all_commits; do
        first_char=$(echo "$commit" | cut -c 1 | tr "[:lower:]" "[:upper:]")

        echo "$commit" | sed -e "s|^.\(.*$\)|- $first_char\1|" -e 's|\(.*[^.!?]\)$|\1.|'
    done
}

get_commits_between() {
    log_format=$1
    latest_git_tag=$2
    latest_log_tag=$3
    needed_tag=$4

    highest_tag=$(printf "%s\n%s\n" "$latest_git_tag" "$needed_tag" |
        sort -Vr | head -1)

    tag_range="$latest_log_tag.."
    [ "$latest_git_tag" = "$highest_tag" ] && tag_range="$latest_log_tag..$needed_tag"

    git log --pretty=format:"$log_format" "$tag_range" | sed "s|$||"
}

#### Utils ####

get_new_tag() {
    latest_changes=$1
    latest_git_tag=$2

    echo "$latest_changes" | filter_major_commits -q && inc_tag_major "$latest_git_tag" && return
    echo "$latest_changes" | filter_minor_commits -q && inc_tag_minor "$latest_git_tag" && return
    echo "$latest_changes" | filter_patch_commits -q && inc_tag_patch "$latest_git_tag" && return
}

format_commits() {
    commits_head=$1
    commits_body=$(echo "$2" | filter_conv_commits)

    list=$(printf "%s\n%s" "$commits_head" "$commits_body")

    dep=$(echo "$list" | filter_major_commits)
    list=$(echo "$list" | filter_major_commits -v)

    feat=$(echo "$list" | filter_minor_commits)
    list=$(echo "$list" | filter_minor_commits -v)

    fix=$(echo "$list" | filter_patch_commits)
    list=$(echo "$list" | filter_patch_commits -v)

    [ "$dep" ] && echo "### BREAKING CHANGE" && fmt_commits "$dep" && echo
    [ "$feat" ] && echo "### Added" && fmt_commits "$feat" && echo
    [ "$fix" ] && echo "### Fixed" && fmt_commits "$fix" && echo
    [ "$list" ] && echo "### Changed" && fmt_commits "$list" && echo
}

get_needed_tags() {
    latest_git_tag=$1
    latest_log_tag=$2

    tags=$(get_git_tags -v)

    ! echo "$tags" | grep -q "^${latest_log_tag}$" && return 0
    echo "$tags" | sed -n "/^${latest_log_tag}$/!p;//q" | sed '1!G;h;$!d'

    latest_changes=$(git log --pretty=format:"%B" "$latest_git_tag..")
    [ ! "$latest_changes" ] && return

    get_new_tag "$latest_changes" "$latest_git_tag" | rm_tag_suffix
}

update_diffs() {
    latest_git_tag=$1
    latest_log_tag=$2
    needed_tag=$3

    latest_log_ver=$(rm_tag_prefix "$latest_log_tag")
    needed_ver=$(rm_tag_prefix "$needed_tag")

    new_commits_head=$(get_commits_between "%s" "$latest_git_tag" "$latest_log_tag" "$needed_tag")
    new_commits_body=$(get_commits_between "%b" "$latest_git_tag" "$latest_log_tag" "$needed_tag")

    new_section="## [$needed_ver] - $(get_tag_date "$needed_tag")\\
$(format_commits "$new_commits_head" "$new_commits_body" | sed -e "s|\([&\\]\)|\\\\\1|g" -e "s|$|\\\\|")"

    regex="s|\(^## \[$latest_log_ver\] -.*$\)|$new_section$nl\1|"
    sed -i.backup "$regex" $change_file && rm "${change_file}.backup"
}

update_links() {
    latest_log_tag=$1
    needed_tag=$2

    latest_log_ver=$(rm_tag_prefix "$latest_log_tag")
    needed_ver=$(rm_tag_prefix "$needed_tag")
    repo_url=$(get_git_url)

    new_link="[$needed_ver]: $repo_url/compare/$latest_log_tag...$needed_tag"

    regex="s|\(^\[$latest_log_ver\]:.*$\)|$new_link\\$nl\1|"
    sed -i.backup "$regex" "$change_file" && rm "${change_file}.backup"

    regex="s|\(^\[Unreleased\]:.*\)/$latest_log_tag\.\.\.HEAD$|\1/$needed_tag...HEAD|"
    sed -i.backup "$regex" "$change_file" && rm "${change_file}.backup"
}

startup_checks() {
    [ ! -e "$change_file" ] && { echo "couldn't find $change_file" >&2; return 1; }
    [ ! -d .git ] && { echo "the current directory doesn't contain .git" >&2; return 1; }
    ! git config --get remote.origin.url >/dev/null &&
        { echo "remote origin url isn't set for this repo" >&2; return 1; }

    return 0
}

bump_version() {
    [ ! "$bump_script" ] && return 0
    latest_log_tag=$(get_latest_log_tag)

    [ -x "$bump_script" ] &&
        { echo "executing $bump_script $latest_log_tag"; "$bump_script" "$latest_log_tag"; return; }

    [ ! -x "$bump_script" ] && [ -f "$bump_script" ] &&
        { echo "executing sh $bump_script $latest_log_tag"; sh "$bump_script" "$latest_log_tag"; return; }

    [ ! -f "$bump_script" ] && echo "script not found: $bump_script" >&2
    return 1
}

#### Public ####

Make_and_push_tag() {
    ! startup_checks && return 1
    latest_log_tag=$(get_latest_log_tag)
    echo "update force tag $latest_log_tag"
    git tag --force "$latest_log_tag" -m "Release version $latest_log_tag" || return 1
    echo "tagged latest commit as $latest_log_tag"

    [ "$push_tag" = "true" ] &&
        #git push --force --quiet origin "$latest_log_tag" &&
        git push --force origin "$latest_log_tag" &&
        echo "pushed $latest_log_tag to origin" &&
        return 0

    echo "to push the latest tag use: git push origin $latest_log_tag"
    echo "to push all local tags use: git push origin --tags"
}

Save_auth() {
    [ ! "$token" ] && echo "enter a personal access token" && printf "token: " && read -r token
    [ ! "$token" ] && { echo "invalid token" >&2; return 1; }
    [ ! -d "$auth_dir" ] && mkdir -p "$auth_dir"

    echo "$token" | base64 > "$auth_file" && echo "git auth saved"
}

Post_release() {
    ! startup_checks && return 1
    [ ! -e "$auth_file" ] && { echo "you need to save a token with the auth" >&2; return 1; }

    auth_token=$(base64 --decode < "$auth_file")
    latest_log_tag=$(get_latest_log_tag)
    latest_log_ver=$(rm_tag_prefix "$latest_log_tag")
    repo_url=$(get_git_url | sed "s|\(^.*\)\(github.com\)|\1api.\2/repos|")

    diff=$(sed -n "/^## \[$latest_log_ver\] -.*/,/^## \[.*\] -.*/P" $change_file | sed "\$d")
    link=$(grep "^\[$latest_log_ver\]: .*" $change_file)
    body=$(echo "$diff$nl$nl$link" | sed -e "s|\([\"\\]\)|\\\\\1|g" -e "s|$|\\\\|" | tr '\n' 'n')

    [ "$dry_run" = "true" ] &&
        printf "%s\n%s\n%s\n" "$repo_url" "$latest_log_tag" "$body" && return 0

    curl -X POST "$repo_url/releases" \
        -H "Authorization: token $auth_token" \
        -H "Content-Type: application/json" \
        -d "{\"tag_name\": \"$latest_log_tag\", \"name\": \"$latest_log_tag\", \"body\": \"$body\" }"
}

Create_changelog() {
    [ ! -d .git ] && { echo "the current directory doesn't contain .git" >&2; return 1; }
    [ -e "$change_file" ] && { echo "$change_file already exists" >&2; return 1; }

    first_tag=$(get_git_tags | head -1)
    [ ! "$first_tag" ] && { echo "couldn't find any valid version tags" >&2; return 2; }

    first_ver=$(rm_tag_prefix "$first_tag")
    [ ! "$first_ver" ] && { echo "couldn't get first version" >&2; return 2; }

    first_commits=$(git log --pretty=format:"%s" "$first_tag")
    [ ! "$first_commits" ] && { echo "couldn't get first commits" >&2; return 2; }

    repo_url=$(get_git_url)
    [ ! "$repo_url" ] && { echo "remote origin url isn't set for this repo" >&2; return 1; }

    echo "# Changelog
All notable changes to this project will be documented in this file.

## [Unreleased]

## [$first_ver] - $(get_tag_date "$first_tag")
$(format_commits "$first_commits")

[Unreleased]: $repo_url/compare/$first_tag...HEAD
[$first_ver]: $repo_url/releases/tag/$first_tag" > "$change_file"

    [ ! -e "$change_file" ] &&
        echo "something when wrong while creating $change_file" >&2 && return 1
    echo "created $change_file"
}

Update_changelog() {
    ! startup_checks && return 1

    latest_git_tag=$(get_git_tags -v | head -1)
    latest_log_tag=$(get_latest_log_tag)
    all_needed_tags=$(get_needed_tags "$latest_git_tag" "$latest_log_tag")

    [ ! "$all_needed_tags" ] && { echo "no new versions to add"; return 1; }

    IFS="$nl"
    for needed_tag in $all_needed_tags; do
        update_diffs "$latest_git_tag" "$latest_log_tag" "$needed_tag"
        update_links "$latest_log_tag" "$needed_tag"
        echo "added $needed_tag to $change_file"

        latest_log_tag="$needed_tag"
    done
    git add $change_file
    git commit -m "update changelog"
    git push

    [ ! "$command" ] && { bump_version; return; }

    return 0
}

Run_all() {
    save_file=$(cat $change_file)
    ! Update_changelog && return 1
    first_change=$(stat -qf "%Sc" $change_file)
    ${EDITOR:-vi} $change_file
    last_change=$(stat -qf "%Sc" $change_file)
    [ "$first_change" = "$last_change" ] &&
        echo "cancelling because no manual edits were made to $change_file" >&2 &&
        echo "$save_file" > $change_file &&
        return 1
    bump_version
    push_tag="true"
    git add $change_file &&
        git commit --quiet -m "docs: update changelog" &&
        git push --quiet origin &&
        Make_and_push_tag && Post_release
}

#### Args ####

command=$1
[ "$(expr "$command" : "-.*")" != 0 ] && command=""
[ "$command" ] && shift

while [ $# != 0 ]; do
    case $1 in
        "-p" ) push_tag="true";;
        "--dry-run" ) dry_run="true";;
        "--bump" ) bump_script="$2"; shift;;
        "--bump="* ) bump_script="${1#*=}";;
        "--token" ) token="$2"; shift;;
        "--token="* ) token="${1#*=}";;
        "--help" | "-h" ) Usage; exit;;
        "--version" | "-v" ) echo "$change_version"; exit;;
        * ) echo "invalid argument: $1" >&2; exit 1;;
    esac

    shift
done


case $command in
    "" ) Update_changelog;;
    "init" ) Create_changelog;;
    "auth" ) Save_auth;;
    "tag"  ) Make_and_push_tag;;
    "post" ) Post_release;;
    "all"  ) Run_all;;
    "help" ) Usage;;
    "version" ) echo "$change_version";;
    * ) echo "invalid command: $command" >&2; exit 1;;
esac

exit
